// ==UserScript==
// @name         自动化框架测试
// @namespace    https://bbs.tampermonkey.net.cn/
// @version      0.1.0
// @description  try to take over the world!
// @author       You
// @match        https://www.xiaohongshu.com/board/*
// @require      https://scriptcat.org/lib/2691/1.0.0/sweetalert2.all.min-11.15.10.js
// ==/UserScript==


class AutomationEngine {
    constructor(config) {
        this.config = config;
        this.taskQueue = [];
        this.currentTask = null;
        this.selectorCache = new Map();
    }

    async initialize() {
        await this._parseConfig();
    }

    async _parseConfig() {
        for (const taskConfig of this.config.tasks) {
            const task = this._createTaskFromConfig(taskConfig);
            this.taskQueue.push(task);
        }
    }

    _createTaskFromConfig(config) {
        switch (config.type) {
            case 'batch_open_posts':
                return new BatchOpenPostsTask(config);
            case 'custom_sequence':
                return new CustomSequenceTask(config);
            default:
                throw new Error(`Unknown task type: ${config.type}`);
        }
    }

    async execute() {
        for (const task of this.taskQueue) {
            this.currentTask = task;
            await task.execute(this._createActionExecutor());
        }
    }

    _createActionExecutor() {
        return {
            click: async (params) => this._executeClick(params),
            scroll: async (params) => this._executeScroll(params),
            wait: async (params) => this._executeWait(params),
            waitForEffect: async (params) => this._executeWaitForEffect(params),
            scrollToElement: async (params) => this._executeScrollToElement(params)
        };
    }


    async _waitForActionEffect(selector) {
        return new Promise((resolve) => {
            new MutationObserver((_, observer) => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve();
                }
            }).observe(document.body, { subtree: true, childList: true });
        });
    }

    async _executeClick({ selector, count = 1, interval }) {
        const resolvedSelector = this._resolveSelector(selector);
        for (let i = 0; i < count; i++) {
            const element = await this._retryFindElement(
                resolvedSelector,
                this.config.settings.default_retry,
                this.config.settings.default_timeout
            );
            element.click();
            if (interval) await this._delay(interval);
        }
    }

    async _executeScrollToElement({ selector }) {
        const resolvedSelector = this._resolveSelector(selector);
        const element = await this._retryFindElement(
            resolvedSelector,
            this.config.settings.default_retry,
            this.config.settings.default_timeout
        );
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        await this._delay(this.config.settings.global_delay);
    }

    async _executeScroll({ direction, threshold = 0.8 }) {
        const scrollPosition = direction === 'bottom'
            ? document.body.scrollHeight * threshold
            : 0;
        window.scrollTo(0, scrollPosition);
        await this._delay(this.config.settings.global_delay);
    }

    async _executeWaitForEffect({ selector }) {
        const resolvedSelector = this._resolveSelector(selector);
        await this._waitForActionEffect(resolvedSelector);
        await this._delay(this.config.settings.global_delay);
    }

    async _executeWait({ duration }) {
        await this._delay(duration);
    }

    _resolveSelector(selector) {
        return selector.startsWith('$')
            ? this.config.tasks.find(t => t.name === selector.slice(1))?.selector
            : selector;
    }
    _delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    async _retryFindElement(selector, maxRetries, timeout) {
        let retries = 0;

        return new Promise((resolve, reject) => {
            const check = () => {
                const element = document.querySelector(selector);
                if (element) {
                    resolve(element);
                } else if (retries < maxRetries) {
                    retries++;
                    setTimeout(check, timeout / maxRetries);
                } else {
                    reject(new Error(`Element not found: ${selector}`));
                }
            };

            check();
        });
    }
}

/**
 * 配置化任务基类
 */
class ConfigurableTask {
    constructor(config) {
        this.name = config.name;
        this.actions = config.actions;
        this.selectorMap = config.config.selectors || {};
    }

    async execute(executor) {
        console.log(`[Task] Starting: ${this.name}`);
        for (const action of this.actions) {
            await this._executeAction(action, executor);
        }
    }

    async _executeAction(actionConfig, executor) {
        const actionType = actionConfig.type;
        const params = this._resolveActionParams(actionConfig);
        await executor[actionType](params);
    }

    _resolveActionParams(config) {
        const params = { ...config };
        // 解析选择器引用
        Object.keys(params).forEach(key => {
            if (typeof params[key] === 'string' && params[key].startsWith('$')) {
                const refKey = params[key].slice(1);
                params[key] = this.selectorMap[refKey];
            }
        });
        return params;
    }
}

/**
 * 具体任务实现
 */
class BatchOpenPostsTask extends ConfigurableTask {
    constructor(config) {
        super(config);
        this.targetCount = config.config.target_count;
        this.postDataIndex = new Map();
    }

    async execute(executor) {
        console.log(`[Task] Starting batch open: ${this.targetCount} posts`);
        let loadedCount = 0;
        let post_array = [];
        let actionScrollToElement = {};
        let actionWaitMask = {};
        actionWaitMask = {
            type: 'waitForEffect',
            selector: this.selectorMap.mask
        };
        while (loadedCount < this.targetCount) {
            let currentIndex = 0;
            post_array = [...document.querySelectorAll(
                this.selectorMap.post_array
            )];
            for (const post of post_array) {
                const postIndex = post.getAttribute('data-index');
                if (!this.postDataIndex.has(postIndex)) {
                    this.postDataIndex.set(postIndex, true);
                    loadedCount++;
                    currentIndex++;
                    this.actions[0].selector = this.selectorMap.post_array + `:nth-child(${currentIndex})` + '>div>a:nth-child(2)';
                    for (const action of this.actions) {
                        /*  如果后续有使用MutationObserver点击后就不用加延迟了，防止动画结束后才创建MutationObserver
                        如果加需调整actionWaitMask代码顺序 */
                        await this._executeAction(action, executor);
                        await this._executeAction(actionWaitMask, executor);
                    }
                }
            }
            actionScrollToElement = {
                type: 'scrollToElement',
                selector: this.selectorMap.post_array + `:nth-child(${post_array.length})`
            };
            await this._executeAction(actionScrollToElement, executor);
            console.log(`Current loaded posts: ${loadedCount}`);
        }
    }
}

class CustomSequenceTask extends ConfigurableTask {
    // 直接使用父类实现
}

const config = // config.json
{
    "tasks": [
        {
            "type": "batch_open_posts",
            "name": "打开36个帖子",
            "config": {
                "target_count": 36,
                "selectors": {
                    "post_array": "div.feeds-container > section",
                    "close_btn": "body > div.note-detail-mask > div.close-circle > div",
                    "mask": "body > div.note-detail-mask"
                },
            },
            "actions": [
                {
                    "type": "click",
                    "selector": "_postEle",
                    // "interval": 100
                },
                {
                    "type": "click",
                    "selector": "$close_btn",
                    // "interval": 100
                },
            ]
        },
    ],
    "settings": {
        "default_retry": 3,
        "default_timeout": 10000,
        "global_delay": 1000
    }
};

const engine = new AutomationEngine(config);
(function () {
    'use strict';
    /**
     * 配置驱动执行系统
     */

    window.addEventListener('load', addPanel);
    // 配置不代表直接按照json直接指向操作，而是配置出自动化任务中的固定行为的属性（延迟，重试）

})();
function addPanel() {
    function genButton(text, foo, id, fooParams = {}) {
        let b = document.createElement('button');
        b.textContent = text;
        b.style.verticalAlign = 'inherit';
        // 使用箭头函数创建闭包来保存 fooParams 并传递给 foo
        b.addEventListener('click', () => {
            foo.call(b, ...Object.values(fooParams)); // 使用 call 方法确保 this 指向按钮对象
        });
        if (id) { b.id = id };
        return b;
    }

    function changeRangeDynamics() {
        const value = parseInt(this.value, 10);
        const roundedValue = Math.ceil(value / 10) * 10;

        targetAmountGlobal = roundedValue;
        // 只能通过 DOM 方法改变
        document.querySelector('#swal-range > output').textContent = roundedValue;
    }

    async function openPanelFunc() {
        let isLoadEnd = false;
        targetAmountGlobal = 100;
        var swalRangeValue = 100;
        setTimeout(() => {
            isLoadEnd = true;
        }, 13000);
        const { value: formValues } = await Swal.fire({
            title: "最大点赞数",
            showCancelButton: true,
            cancelButtonText: '取消',
            confirmButtonText: '确定',
            //class="swal2-range" swalalert框架可能会对其有特殊处理，导致其内标签的id声明失效
            html: `
              <div class="swal2-range" id="swal-range" style="display: flex;">
                <input type="range" min="0" max="500" step="10" value="100">
                <output>${swalRangeValue}</output>
              </div>
            `,
            focusConfirm: false,
            didOpen: () => {
                const swalRange = document.querySelector('#swal-range input');
                swalRange.addEventListener('input', changeRangeDynamics);
            },
            willClose: () => {
                // 在关闭前清除事件监听器以防止内存泄漏
                const swalRange = document.querySelector('#swal-range input');
                swalRange.removeEventListener('input', changeRangeDynamics);
            },
            preConfirm: () => {
                return [
                    targetAmountGlobal
                ];
            }
        });
        if (formValues) {
            await engine.initialize();
            await engine.execute();
        }
    }

    let myButton = genButton('CMlike', openPanelFunc, 'CMlike');
    document.body.appendChild(myButton);

    var css_text = `
        #CMlike {
            position: fixed;
            color: rgb(211, 67, 235);
            top: 70%;
            left: -20px;/* 初始状态下左半部分隐藏 */
            transform: translateY(-50%);
            z-index: 1000; /* 确保按钮在最前面 */
            padding: 10px 24px;
            border-radius: 5px;
            cursor: pointer;
            border: 0;
            background-color: white;
            box-shadow: rgb(0 0 0 / 5%) 0 0 8px;
            letter-spacing: 1.5px;
            text-transform: uppercase;
            font-size: 9px;
            transition: all 0.5s ease;
        }
        #CMlike:hover {
            left: 0%; /* 鼠标悬停时完整显示 */
            letter-spacing: 3px;
            background-image: linear-gradient(to top, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%);
            box-shadow: rgba(211, 67, 235, 0.7) 0px 7px 29px 0px; /* 更柔和的紫色阴影，带透明度 */
        }
        
        #CMlike:active {
            letter-spacing: 3px;
            background-image: linear-gradient(to top, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%);
            box-shadow: rgba(211, 67, 235, 0.5) 0px 0px 0px 0px; /* 活动状态下的阴影，保持一致性 */
            transition: 100ms;
        }
    `
    GMaddStyle(css_text);
}

function GMaddStyle(css) {
    var myStyle = document.createElement('style');
    myStyle.textContent = css;
    var doc = document.head || document.documentElement;
    doc.appendChild(myStyle);
}